Rust on Zephyr Roadmap

May 2025 is the conclusion of the initial effort to bring support for Rust to Zephyr. At this point, the following are supported:

  • Bindgen generates bindings to the zephyr includes configured for this specific Zephyr build.
  • Zephyr threads. A proc macro #zephyrthread can decorate a top level function (with Send+Sized arguments) to make a function that instead creates a Zephyr thread and returns a handle that can be used to start the thread and manage it.
  • Various zephyr primitives have safe wrappers: Semaphore, Mutex/Condvar, Queue.
  • Higher level sync primitives: Mutex/Condvar, Queue (the wrapped versions are safe, but not particularly useful to Rust code).
  • Zephyr timers, that an be used with any of a blocking wait, polled, or with a callback.
  • Work queues can be declared, and Work items written and managed in Rust code.
  • A simple log wrapper. Logs from the log crate will be send either to printk, or to Zephyr's log mechanism.
  • A time module that implements Duration and Time types. These can be used in most API calls that expect a timeout.
  • Kconfig support. String and Numeric values are exposed under zephyr::kconfig::* and boolean values can be made visible with a simple call in build.rs.
  • Devicetree Support. The build's devicetree is made available under zephyr::devicetree with a module structure mirroring the devicetree. Presence conditional checks for nodes can be made visible via a call in build.rs.
  • Simplistic device support:
    • Simple gpio, setting, reading, and a simple wait_for_high/low async call.
    • Simple interface to the flash driver.
    • Simple interface to uart.
    • Simple interface to i2c - controller.
  • An optional implementation of an async executor built on embassy-executor.
  • An optional implementation of embassy-time-driver. Combined, these allow most functionality from embassy-sync, embassy-futures, and many other async no-std crates to directly be used on Zephyr.

Ongoing Maintenance

At any point that there is upstream support within Zephyr, there is a need to maintain this code. This involves:

  • Responding to reported issues, both from users, and discovered by CI.
  • Responding to security vulnerability reports. This is similar to issue, but with additional requirements from the Zephyr security incident process.
  • Adapting to changes made within Zephyr. One concern raised over supporting Rust within Zephyr is that there may be failures in Rust code when changes are made in the Zephyr C code base, especially when the developer changing the C code is not familiar with Rust. There should be someone available to assist with these kinds of changes.
  • Participating in the communinity, including reviewing and merging patches to the Rust code made by the community.

In addition to the maintenance work, there are likely to be new features added to Zephyr that will require additional work to support.

Estimate: 10-20% of an engineer for maintenance.

New feature support is difficult to estimate.

Additional Work

With the work that is done, it is possible to write multi-thread or async programs on Zephyr. However, Zephyr contains significantly more functionality, in terms of both a multitude of drivers as well as a large number of subsystems.

The additional work can be placed into two categories: Flushing out the implemented functionality to be more complete, and possibly more usable from Rust, and adding support for drivers and subsystems.

Without additional work, it is possible for user to make use of other parts of Zephyr, however this approach misses out on many of the benefits of using Rust. To do this requires:

  • Add patterns and includes to bindgen.
  • Use unsafe calls directly to the zephyr operations.
  • Possibly create safer wrappers. At this point, this should probably be encouraged to be contributed to the project.

Stabilization

At some point, an effort should be made to stabilizes the interfaces. This should wait until the Rust/Zephyr interfaces have had some noticeable use, giving an opportunity to go through improvements without needing more involved deprecation processes.

Rust, unlike Zephyr, uses semantic versions. The current version is marked "0.1.0", indicating that it is unstable, and API changes can be expected. Once "1.0.0" is released, any changes to the API will need to undergo a deprecation process, and even compatible changes will need to track changes according to the rules of semantic versioning.

This should be undertaken after a majority of the work is finished.

Estimate: 4 weeks

Flushing out

The following tasks around the existing functionality will make the experience of using them more complete.

Improving Documentation

The Zephyr interfaces are primarily documented through the "rustdoc" mechanism, where the documentation is in comments. There may be some value in creating documentation covering broader aspects, such as how to work with a particular subsystem, and such.

Estimate: 2 weeks

Generalizing Bindgen

Currently, in order to add bindings, a user must modify a few files within the rust-lang-zephyr repo, specifically within the zephyr-sys crate. These include:

  • wrapper.h - Add include directives for new apis. In addition, some #define values that are expressions have to be explicitly declared via consts for bindgen to know they have constant values and to be made visible. Occasionally, static functions might also have to be written.
  • build.rs - There is a list of patterns of symbols that bindgen uses. Depending on the names of the API, this list may need to be extended.

It is unclear how this can be generalized by a user of the crate, and it probably makes sense to be fairly free in accepting contributions that add patterns here. The cost of additional patterns in slowing down the compilation slightly as the generated bindings will be larger. Unlike C, however, this is only done once per build, not for each file that uses the bindings.

Estimate: 0 weeks

Logging

There is an inherent semantic mismatch between how logging works in Rust vs Zephyr, mostly a consequence of the difference in how formatting strings work. Zephyr tries to delay formatting of log messages, but because C formatting typically only works on types that fit in registers, there aren't lifetime concerns with these values. Strings are handled as a special case in Zephyr, and the contents of the string are copied into the log buffer.

Rust string formatting, on the other hand, is much richer, allowing constructs such as "{:?}" to format an arbitrary object using the Debug trait. Combined with the compiler support to automatically derive implementations of this trait, this is a powerful tool when debugging. The compile is able to generate these automatic derivations, and the code only ends up in the final image when actually used in a message.

In the rust-embedded world, another crate, defmt is commonly used for logging. This takes a very different approach. The static parts of strings for messages are not included in the image (they are placed in a linker section that is included in the ELF file, but not the on-target image), and the log messages are encoded in a compact format. There is host-side tooling to decode the messages, by reading the message stream, and finding the strings in the ELF file.

From discussions with the defmt developers: they recommend something like the following approach:

  • Add a defmt back end to the zephyr crate. There are a few options to where these messages can be logged. An easy solution would be to allocate an additional channel in the RTT protocol just for defmt messages. This would allow existing zephyr printk or logging to continue to go through the textual channel, while the binary defmt channel would log those messages. The defmt developers are open to patches to their defmt-print tool to allow it to read both streams.
  • Another approach is to add an encoding for defmt messages to be included in the textual logging. This could be as simple as an escape to enter defmt mode with some simple protocol. Again, defmt-print could be enhanced to understand this protocol and decode both streams of data.
  • Additionally, it could be possible to make a Zephyr log backend that logs through the defmt, either just as strings, or with the format string. This is a varying amount of work depending on how extensive the protocol-size savings that is desired.

Trying to match the log crate formatting better to Zephyr's log mechanism is also possible. In general, this will likely always result in mostly pre-formatting the messages. It might also be possible to decode format strings at compile time, and convert some cases of format strings to a printf equivalent, allowing them to better co-exist with Zephyr's logs.

Ideally, the community would likely benefit from both solutions being implemented, allowing individual users the choice.

Estimate: 2-8 weeks, depending on options chosen.

Userspace

Currently, it is not possible to enable CONFIG_USERSPACE while also supporting Rust. This is mostly because the Rust code makes fairly extensive use of atomics and critical sections to maintain safety. It is arguable that the Zephyr Userspace feature is somewhat redundant with the memory protection provided by using Rust. However, it can also make sense to use userspace to protect a program partially written in rust from parts of the code written in other languages.

There are two approaches here, with different degrees of difficulty:

  • Basic: Allow CONFIG_USERSPACE to be defined, but require all threads running Rust code to be in system mode. Implementing this is a matter of documentation, and removing the current build-time check against CONFIG_USERSPACE being defined.

  • Full: To allow Rust code to run in userspace, several things will have to be done:

    • The mechanimsms implementing atomics and spinlocks will have to have alternate implementations that work with userspace mechanisms. Atomics are usable in userspace—in a platform dependent way (some platforms only use intrinsic atomic instructions and allow memory barriers to be used from userspace, other platforms implement atomics using spinlocks, which are not available in userspace). Critical sections would likely have to be implementing using a Mutex. There will likely be a performance impact due to this implementation.
    • Rust friendly interfaces to Zephyr's permission operations will need to be made. This is needed to control what resources are accessible to userspace threads. In addition, any mechanimsms that use zephyr primitives "under the hood" will need to have permission APIs added, as these will need to grant priveleges to the underlying operations.
    • Userspace in Zephyr requires that all primitive objects be declared statically, and be placed in specific link sections. Currently, Zephyr primitives are bundled with an atomic to enforce correct initialization. This approach would have to be rethought, so that the actual zephyr object would be by itself in its own linker section, and the Rust-side object would contain a reference to it. There would be a memory use increase, and a performance penalty, due to the additional dereference needed for these types. In addition, compound object that contain these primitives would have to be reworked to support these declarations as well.
    • Any interfaces that use callbacks will need a different mechanism. This mostly impacts async code (see below).

Estimate: 1 week for basic, 4-8 weeks for Full.

Deeper Async

Although the current async support is fully featured, despite the simplicity of the implementation, there is a bit of a barrier between async code, and the various Zephyr primitives. As there are several readily-available synchronization crates available that work well from async mode, this is sufficient for code that is completely written in Rust.

However, many interfaces in Zephyr use Zephyr primitives for synchronization. Specifically, semaphores are used fairly pervasively. In addition, using callbacks is not possible with userspace, so it is desirable to be able to use these other mechanisms.

A small bit of background as to how async works in Rust will be helpful. Any time the async keyword is used in Rust, the compiler converts this to a block of code that returns a (hidden definition) structure that implements Future. This trait has requires two things, one is an Output type which defines the final return type when the future has completed, and the other is a poll function. The poll function runs the async code until it reaches a blocking point, or finishes. It returns a value indicating which is the case. The implementation is responsible for using the Context parameter passed to the future, specifically, it's Waker, and to arrange for that Waker to be called when the Future is able to run again.

There are several possibilities for how to implement an async-able Semaphore.

  • Enhance the executor to be aware of semaphores. This is the approach used to desktop/server async implementations (such as Tokio). This will add a fixed cost to all async operations.
  • Use triggered work on a workqueue (possibly just the system work queue) where each registered work item invokes the necessary wake function when that resource becomes available. This makes waiting for Semaphores a bit more expensive, but does not increase the cost for any other async operations.
  • Add callback support to the Zephyr implementation of Semaphores. This is likely to meet resistance from the C-focused core kernel developers in Zephyr, but would be the most efficient. There is some complexity to making this work with userspace, and would be the first Rust-specific mechanism introduced to the core of Zephyr.

I recommend starting with the triggered workqueue approach, as this has the least impact on both other async code, and the core of Zephyr. If/when Rust on Zephyr becomes more common, arguments in favor of adding deeper support will be easier to make.

Estimate: 3 weeks for triggered work, 5 weeks for adding support to callbacks (not counting time getting the RFC through).

IRQ Async

The embassy-executor implementation for some architectures includes an executor that is capable of running async tasks from interrupt context. This is generally its own interrupt (e.g. most Cortex-M implementations have specific interrupts designed for software use). Implementing this in Zephyr requires registering an interrupt handler, and implementing a Zephyr-specific executor to run in this context.

This executor will allow for certain high priority operations to "await", being resumed in the high priority context.

It is unclear if this has benefits over just running an executor in a high priority thread. The irq handler will have overhead beyond bare-metal because the IRQ handlers need to be able to work with the Zephyr scheduler.

Estimate: 4 weeks

Better DeviceTree Integration

Currently, the devicetree support is driven by parsing the generated zephyr.dts file, combined with a rust-specific config file that specifies the Rust device wraper types associated with various nodes in the tree.

Ideally, this information would actually be part of the devicetree schema in Zephyr. There may also be improvements to use, not the generated dts file, but the data structures built by the edtlib parser that Zephyr uses to parse the device tree.

Some parts of this task:

  • Change the Rust DT generation to read the edtlib parsed tree. This involves extending the generator in the Rust build to not just generate a Python pickle file of this data, but also output it in a more portable format, such as json, or yaml, that can be read from the Rust code that generates the Rust devicetree code.
  • Work with the Zephyr DT maintainers to figure out the best way to represent the type information needed by Rust. This would be in parallel with ongoing work to improve how the DT scheme is represented in Zephyr.

Much will be driven by the needs of specific drivers as they are implemented.

Estimate: 2 weeks for using edtlib tree. Undetermined for additional work.

Device

Adding device support for Zephyr involves the following:

  • Analyzing the API for the device.
  • Determine what DT nodes are appropriate, and extending the DT generator for these.
  • Writing a blocking API that generally fairly directly calls the driver. This is provided the device provides a blocking API.
  • Writing an async API. This will depend on how the async interface works for this device.

For some devices, it might make sense to add Zephyr rtio support to that device, so that the existing RTIO support can be used to leverage adding async operations to that driver.

Some areas this might make sense to do:

  • Clock Control
  • Comparator
  • DMA (depending on whether this is usable by itself, or just part of other drivers)
  • eeprom
  • haptics
  • led_strip
  • pwm
  • regulator
  • rtc
  • stepper

A more detailed breakdown can be determined on a per-device basis, generally requiring the device API analisys to be done first.

Estimate: 1 week or less to analyze an API per device, and provide a more detailed estimate.

Subsystem

Zephyr includes numerous subsystems that would be useful from within Rust. Some of these are likely to be better served by just using existing Rust implementations (e.g. http, json, cbor). Others, however, are tightly coupled with Zephyr's drivers, and would work best as a direct interface.

Network

Zephyr's network stack tries to present a posix-inspired interface to networking. At the time of writing, there is a draft PR that adds some initial support for interfacing directly to this.

This interface is largely blocking. There is a poll API in Zephyr, and using this for async would be similar to what is needed to add async support to Semaphores. However, triggered work does not currently support polling sockets, so this would need to add either a separate thread, or could dive under the hood a bit, and leverage the semaphores used by the Zephyr networking stack.

Estimate: 6-12 weeks

USB

Zephyr's USB subsystem is actually several different subsystems.

  • host: Not yet analyzed
  • usb_c: Not yet analyzed
  • device: This is a deprecated subsystem, and new work should be done with device_next.
  • device_next: This is the new generation of device modeling. Instead of a fixed configuration, where the various classes manipulate the descriptors themselves, the new generation requires the user to provide the descriptors directly. This is generally done with a large number of C macros, which would not be usable from Rust. However, there are a series of Rust crates around Rust support for usb devices, such as usbd-hid, usbd-serial, etc, as well as usb-device that likely could provide equivalent functionality. The work would then involved wrapping the device_next apis as well as the apis for the various subsystems.

The USB interface APIs work primarily around callbacks, which would work well with async in Rust.

Estimate: 4-8 weeks for device_next. Unknown for others.

Crypto

Analysis: TBD

Note that there are numerous crates that implement cryptography in Rust, which would be available, likely unmodified in Rust code. Interfacing to Cryptography, or TEE provider code would be needed to make use of things like hardware accelerated crypto.

Others

There are various other subsystems that would needed to be analyzed further to understand the work involved.

  • Bluetooth/BLE
  • Clock Control
  • Comparator
  • DMA
  • eeprom
  • haptics
  • led_strip
  • pwm
  • regulator
  • rtc
  • stepper
  • IPC
  • zbus

External Adjacent Projects

There are some external projects, especially in the Rust embedded community that would be useful to those using Rust on Zephyr.

Embassy Boot

The embassy project has developed a bootloader, currently with functionality similar to a subset of MCUboot. This project may be of interest notably because it is entirely written in Rust, and might help with analysis of safety and security of the code.

Beyond just initially making it work (it is bare-metal), there are some additional tasks that would increase its usefulness:

  • Porting it to run on Zephyr. This will require presenting an interface to flash devices that compilies with the embedded-storage interface. There is a bit of complexity here because embedded storage makes write and erase sizes compile-time constants, so will have to be configured to specific devices, even though the sizes are dynamically determined within Zephyr.
  • More extensive testing. A test framework modeled after the 'sim' from mcuboot would help to ensure that the bootloader is able to handle bad power down cases, and other bugs.
  • SUIT support. A Rust bootloader could be a good place to add support for SUIT manifests. This is likely to be less work than adding to mcuboot as the architecture is simpler, and the interfaces are cleaner due to the abstractions available in Rust.

Estimate: 2 weeks to port to Zephyr. 4 weeks for extensive testing. 8 weeks for SUIT